
#pragma once
#include <iostream>
#include <pthread.h>
#include <vector>
#include <string>
using namespace std;
#include <queue>
#include <ctime>
#include <unistd.h>

// 对线程的属性做一下封装， 有利于线程池的保存以及后面的处理
struct ThreadInfo
{
    pthread_t tid_;
    string name_;
};

template <class T>
class ThreadPool
{
    static const int defaultnum = 5; // 默认的线程池的大小（线程池的大小就是里面包含的线程的数量）

private:
    // 加锁解锁
    void Lock()
    {
        pthread_mutex_lock(&mutex_);
    }

    void Unlock()
    {
        pthread_mutex_unlock(&mutex_);
    }

    // 唤醒线程， 线程是可以被挂起的（就比如信号量）。 当任务没有的时候，线程就要被挂起， 有任务后再唤醒
    void Wakeup()
    {
        pthread_cond_signal(&cond_);
    }

    void ThreadSleep()
    {
        pthread_cond_wait(&cond_, &mutex_);
    }

    bool IsQueueEmpty()
    {
        return tasks_.empty();
    }

public:
    // 线程要执行的函数
    static void *HandlerTask(void *args)
    {
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
        while (true)
        {
            tp->Lock();
            while (tp->tasks_.empty())
            {
                tp->ThreadSleep(); // 如果队列里面没有任务了， 就让线程去休眠。
            }
            // 否则就去拿到tasks里面的任务
            T t = tp->tasks_.front();
            tp->tasks_.pop();

            //
            tp->Unlock();
            t(); // 每一个线程先对任务进行消费， 消费完成之后处理任务。
        }
    }

    // 运行这个线程池， 也就是先将线程创建出来。 然后去运行线程
    void Start()
    {
        int num = threads_.size();
        for (int i = 0; i < num; i++)
        {
            threads_[i].name_ = "thread-";
            threads_[i].name_ += to_string(i);
            pthread_create(&(threads_[i].tid_), nullptr, HandlerTask, this);
        }
    }

    // 主线程给线程池发送任务， 注意， 这个任务一定是可以被储存起来的。 因为当我们的任务很多很多的时候， 我们的线程池内的线程要一个一个地对这些任务进行处理
    void Push(const T &t)
    {
        Lock();
        tasks_.push(t);
        Wakeup();

        Unlock();
    }

    // 获取单例
    // 改编成单例的步骤里面只有这里要说一下， 就是为什么我们要套双层判断。 其实这里的最外面的一层的判断是我们另外加上去的。 为什么
    // 要加这个判断呢？ 就是如果我们不加最外层这一层判断。 那么每一个线程获取单例都要申请所，加锁。 不就是相当于所有的线程都在串行执行？ 这就有效率问题。
    // 解决方案就是这个再加一层判断。 这样假如有四个线程。 那么一开始四个线程都在判断， 那么它们四个线程都进入了if里面。 然后就都申请锁， 但是只有第一个线程能够
    // 进入第二层里面， 其他的进入不了。 那么当这一轮的四个线程都申请一次锁候就都退出了函数， 然后就都去做自己的事情了。 问题是， 当下次它们再来申请单例对象的时候它们连
    // 第一层判断都成功不了了， 也就都不用加锁解锁了， 这就大大提高了效率！！！
    static ThreadPool<T> *GetInstance(int num = defaultnum)
    {
        if (tp_ == nullptr)
        {
            pthread_mutex_lock(&tp_->lock_);

            if (tp_ == nullptr)
            {
                tp_ = new ThreadPool<T>(num);
            }

            pthread_mutex_unlock(&tp_->lock_);
        }

        return tp_;
    }

private:
    // 构造函数私有化， 只有Getinstance里面才能创建。
    ThreadPool(int num = defaultnum)
        : threads_(num)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }

    // 单例模式只有一个对象， 所以要将拷贝构造和拷贝赋值封住， 为了防止有人在外部重新拷贝一个对象。
    ThreadPool(const ThreadPool<T> &tp) = delete;
    const ThreadPool<T> &operator=(const ThreadPool<T> &tp) = delete;
    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }

private:
    vector<ThreadInfo> threads_; // 线程都维护在vector当中， 这个就是线程池里面的线程的个数，
    queue<T> tasks_;             // 向线程池中发送任务， 这个队列里面保存的就是我们的任务的数目。

    pthread_mutex_t mutex_; // 锁，用来生产者线程（本份代码只是主线程）给线程池发送任务时候加锁使用以及消费者线程抢夺任务时加锁使用

    pthread_cond_t cond_; // 条件变量， 用来没有任务的时候，消费者要挂起。

    static pthread_mutex_t lock_; // 锁， 这个锁是为了在获取单例的时候能够让线程原子性的访问if (tp_ == nullptr)。
    static ThreadPool<T> *tp_;    // tp指针， 这就是唯一个单例对象。
};

template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;

template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;